package wator.model.strategies;

import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import wator.model.Agent;
import wator.model.IntegerVector;
import wator.model.MovementStrategy;
import wator.model.Neighborhood;
import wator.model.Simulation;
import wator.model.Neighborhood.Neighbor;

/**
 * This movement strategy applies a force vector model to locomotion decisions.
 * The direction is the weighted average over a set of behaviors, such that
 * predator avoidance, prey attraction, or flocking. Weights are parameterized.
 */
public class WeightedForceVectorStrategy implements MovementStrategy {

	@SuppressWarnings("unused")
	private final static Logger log = Logger.getLogger("MovementStrategy");
	
	int flockingWeight = 20;
	int inertiaWeight = 10;
	double predatorAvoidanceWeight = 100;
	int preyAttractionWeight = 50;
	int energyAttractionWeight = 50;
	double separationRange = 5;
	double maxVelocity = 3;
	double detectionRange; // Double for easier computation, values are integer
	RandomMovementStrategy randomStrategy;
	boolean onSpeed;
	
	int totalWeight = 0; // Accumulator used in apply() and helper methods

	
	/**
	 * Sole constructor. Initializes the underlying random movement strategy
	 * and other initial variables.
	 * 
	 * @param onSpeed defines if the moving strategy is in "on speed" mode, i.e.
	 * 		always trying to maximize its own velocity
	 */
	public WeightedForceVectorStrategy(boolean onSpeed) {
		super();
		this.onSpeed = onSpeed;
		detectionRange = Math.sqrt(Simulation.getInstance()
				.getPopulation().getNeighborhoodRadius2());
		randomStrategy = new RandomMovementStrategy();
		randomStrategy.setParameter("Range", (int) detectionRange);
	}

	/* (non-Javadoc)
	 * @see wator.model.MovementStrategy#apply(wator.model.Agent, wator.model.Neighborhood)
	 */
	@Override
	public IntegerVector apply(Agent agent, Neighborhood neighborhood) {
		totalWeight = inertiaWeight;
		IntegerVector dir = agent.getDirectionVector().mul(inertiaWeight);
		for (Neighbor n : neighborhood) {
			IntegerVector dist = n.getDistanceVector();
			Agent other = n.getAgent();
			if (other.isOnMenu(agent)) { // Avoid predators
				dir = dir.add(avoidPredators(dist));
			}
			if (agent.isOnMenu(other)) { // Hunt
				dir = dir.add(hunt(dist, other.getEnergy()));
			}
			if (other.getClass().equals(agent.getClass())) { // Same species
				dir = dir.add(flock(dist, other.getDirectionVector()));
			}
		}
		if (totalWeight != 0) {
			dir = dir.mul(1f / totalWeight);
		}
		if (dir.length2() == 0) {
			dir = randomStrategy.apply(agent, neighborhood);
		}
		final double length = dir.length();
		if (onSpeed || length > maxVelocity) {
			dir = dir.mul(maxVelocity / length);
		}
		return dir;
	}
	
	/* Helper method for apply(). Maximum velocity away from predators.
	 * 
	 * @param dist distance vector to the other individual
	 * @return the movement vector for realizing this behavior
	 */
	private IntegerVector avoidPredators(IntegerVector dist) {
		totalWeight += predatorAvoidanceWeight;
		return dist.mul(-predatorAvoidanceWeight / dist.length() * maxVelocity);
	}
	
	/* Helper method for apply(). Hunt prey.
	 * 
	 * @param dist distance vector to the other individual
	 * @param energy energy of the other species
	 * @return the movement vector for realizing this behavior
	 */
	private IntegerVector hunt(IntegerVector dist, int energy) {
		int weight = preyAttractionWeight + energyAttractionWeight * energy;
		totalWeight +=  weight;
		return dist.mul(weight);	
	}
	
	/* Helper method for apply(). Implements flocking.
	 * 
	 * @param dist distance vector to the other individual
	 * @param otherDir direction vector of the other individual
	 * @return the movement vector for realizing this behavior
	 */
	private IntegerVector flock(IntegerVector dist, IntegerVector otherDir) {
		double distance = dist.length();
		double weight = 0;
		if (distance <= separationRange) {
			weight = 1.0 - distance / separationRange;
		} else {
			weight = (distance - separationRange) /
					(detectionRange - separationRange);
		}
		weight *= weight * 2000f;
		IntegerVector flockVector = dist.mul(weight * 
				(distance - separationRange) / distance);
		if (otherDir.length2() > 0) {
			flockVector = flockVector.add(otherDir.mul(1000));
			totalWeight += flockingWeight * 1000;
		}
		totalWeight += flockingWeight * Math.abs(weight);
		return flockVector.mul(flockingWeight);
	}

	/* (non-Javadoc)
	 * @see wator.model.MovementStrategy#setParameter(java.lang.String, int)
	 */
	@Override
	public void setParameter(String key, int value) {
		if (key.equals("Flocking")) {
			flockingWeight = value;
		} else if (key.equals("Predator Avoid.")) {
			predatorAvoidanceWeight = value;
		} else if (key.equals("Prey Attraction")) {
			preyAttractionWeight = value;
		} else if (key.equals("Energy Attraction")) {
			energyAttractionWeight = value;
		} else if (key.equals("Separation Range")) {
			separationRange = value;
		} else if (key.equals("Inertia")) {
			inertiaWeight = value;
		} else if (key.equals("Max. Velocity")) {
			maxVelocity = value;
		}
	}

	/* (non-Javadoc)
	 * @see wator.model.MovementStrategy#getParameters()
	 */
	@Override
	public Map<String, Integer> getParameters() {
		HashMap<String, Integer> params = new HashMap<String, Integer>();
		params.put("Flocking", flockingWeight);
		params.put("Predator Avoid.", (int) predatorAvoidanceWeight);
		params.put("Prey Attraction", preyAttractionWeight);
		params.put("Energy Attraction", energyAttractionWeight);
		params.put("Separation Range", (int) separationRange);
		params.put("Inertia", inertiaWeight);
		params.put("Max. Velocity", (int) maxVelocity);
		return params;
	}

	/* (non-Javadoc)
	 * @see wator.model.MovementStrategy#getName()
	 */
	@Override
	public String getName() {
		return "Force Vector" + (onSpeed ? " On Speed" : "");
	}
}
